package left42dead;

import java.util.Random;
import java.util.List;
import java.util.ArrayList;
import java.awt.Color;

/**
 * A simple predator-prey simulator, based on a rectangular field containing
 * characters and objects.
 * 
 * @author David J. Barnes and Michael Kolling
 */
public class Simulator
{
    // Constants representing configuration information for the simulation.
    // The default width for the grid.
    private static final int DEFAULT_WIDTH = 50;
    // The default depth of the grid.
    private static final int DEFAULT_DEPTH = 50;
    // The probability that a human will be created in any given grid position.
    private static final double HUMAN_CREATION_PROBABILITY = 0.06;
    // The probability that a vampire will be created in any given grid position.
    private static final double VAMPIRE_CREATION_PROBABILITY = 0.001;
    // The probability that a zombie will be created in any given grid position.
    private static final double ZOMBIE_CREATION_PROBABILITY = 0.02;
    // The probability that a meal appears each turn
    private static final double FOOD_DROP_PROBABILITY = 0.8;
    // The probability that a shotgun appears each turn
    private static final double SHOTGUN_DROP_PROBABILITY = 0.2;
    // The probability that a nitro appears each turn
    private static final double NITRO_DROP_PROBABILITY = 0.3;
    // The probability that a stake appears each turn
    private static final double STAKE_DROP_PROBABILITY = 0.8;
    // The probability that ammo appears each turn
    private static final double AMMO_DROP_PROBABILITY = 0.2;
    // The number of turn skipped for humans
    private static final int HUMAN_SPEED = 2;
    // The number of turn skipped for vampire
    private static final int VAMPIRE_SPEED = 1;
    // The number of turn skipped for zombie
    private static final int ZOMBIE_SPEED = 3;
    // The default human health points
    private static final int HUMAN_DEFAULT_HP = 100;
    // The default vampire health points
    private static final int VAMPIRE_DEFAULT_HP = 120;
    // The default zombie health points
    private static final int ZOMBIE_DEFAULT_HP = 30;

    // Lists of characters in the field.
    private List<Character> characters;
    // The current state of the field.
    private Field field;
    // The current step of the simulation.
    private int step;
    // The maximum step of the simulation.
    private int maxStep;
    // A graphical view of the simulation.
    private SimulatorView view;

    /**
     * Construct a simulation field with default size.
     */
    public Simulator ()
    {
        this(DEFAULT_DEPTH, DEFAULT_WIDTH);
    }

    /**
     * Create a simulation field with the given size.
     * 
     * @param depth Depth of the field. Must be greater than zero.
     * @param width Width of the field. Must be greater than zero.
     */
    public Simulator (int depth, int width)
    {
        if (width <= 0 || depth <= 0)
        {
            System.out.println("The dimensions must be greater than zero.");
            System.out.println("Using default values.");
            depth = DEFAULT_DEPTH;
            width = DEFAULT_WIDTH;
        }

        characters = new ArrayList<Character>();
        field = new Field(depth, width);

        // Create a view of the state of each location in the field.
        view = new SimulatorView(depth, width);
        view.setColor(Human.class,          Color.blue   );
        view.setColor(Vampire.class,        Color.red    );
        view.setColor(Zombie.class,         Color.green  );
        view.setColor(Food.class,           Color.magenta);
        view.setColor(Ammo.class,           Color.gray   );
        view.setColor(Shotgun.class,        Color.black  );
        view.setColor(WoodenStake.class,    Color.orange );
        view.setColor(LiquidNitrogen.class, Color.cyan   );

        // Setup a valid starting point.
        reset();
    }

    /**
     * Run the simulation from its current state for a reasonably long period,
     * e.g. 500 steps.
     */
    public void runLongSimulation ()
    {
        simulate(500);
    }

    /**
     * Run the simulation from its current state for the given number of steps.
     * Stop before the given number of steps if it ceases to be viable.
     * 
     * @param numSteps The number of steps to run for.
     */
    public void simulate (int numSteps)
    {
        // Set maxStep
        maxStep = numSteps;

        for (int step = 1; step <= numSteps && view.isViable(field); step++)
        {
            simulateOneStep();

            // Pause
            try { Thread.sleep(100); } catch (InterruptedException exc) {}
        }
    }

    /**
     * Run the simulation from its current state for a single step. Iterate over
     * the whole field updating the state of each fox and rabbit.
     */
    public void simulateOneStep ()
    {
        Random rand = Randomizer.getRandom();
        // Provide space for newborn characters
        List<Character> newCharacters = new ArrayList<Character>();
        // Let all characters act
        for (int i = 0; i < characters.size(); ++i)
        {
            Character character = characters.get(i);

            // Check humans states
            if (character instanceof Human)
            {
                Human human = (Human) character;

                // If bitten by a vampire
                if (human.getHasBeenBitten())
                {
                    Vampire newVampire = human.turnIntoVampire();
                    characters.set(i, newVampire);
                    character = newVampire;
                }
                // If infected by a zombie
                else if (human.getHasBeenInfected())
                {
                    Zombie newZombie = human.turnIntoZombie(ZOMBIE_DEFAULT_HP);
                    characters.set(i, newZombie);
                    character = newZombie;
                }
            }

            // Run character turn if he's not dead and
            // skip turn to simulate character speed
            if (!character.isDead() &&
                    ((character instanceof Human   && step % HUMAN_SPEED   == 0) ||
                     (character instanceof Vampire && step % VAMPIRE_SPEED == 0) ||
                     (character instanceof Zombie  && step % ZOMBIE_SPEED  == 0))
               )
            {
                character.run(newCharacters);
            }

            // End of turn
            if (character.isDead())
            {
                characters.remove(i--);
                
                // Remove it from field
                if (field.getObjectAt(character.getLocation()) == character)
                    field.clear(character.getLocation());
            }
            else
                character.endOfTurn();
        }

        // Create randomly items at end of turn
        // Food
        if (rand.nextDouble() <= FOOD_DROP_PROBABILITY)
        {
            Location loc = field.getFreeRandomLocation();
            // Check field full
            if (!(loc == null))
                field.place(new Food(50), loc);
        }
        // Shotgun
        if (rand.nextDouble() <= SHOTGUN_DROP_PROBABILITY)
        {
            Location loc = field.getFreeRandomLocation();
            // Check field full
            if (!(loc == null))
                field.place(new Shotgun(7), loc);
        }
        // Ammo
        if (rand.nextDouble() <= AMMO_DROP_PROBABILITY)
        {
            Location loc = field.getFreeRandomLocation();
            // Check field full
            if (!(loc == null))
                field.place(new Ammo(7), loc);
        }
        // Stake
        if (rand.nextDouble() <= STAKE_DROP_PROBABILITY)
        {
            Location loc = field.getFreeRandomLocation();
            // Check field full
            if (!(loc == null))
                field.place(new WoodenStake(), loc);
        }
        // Nitro
        if (rand.nextDouble() <= NITRO_DROP_PROBABILITY)
        {
            Location loc = field.getFreeRandomLocation();
            // Check field full
            if (!(loc == null))
                field.place(new LiquidNitrogen(), loc);
        }

        // Add the newly born humans to the main lists.
        characters.addAll(newCharacters);

        step++;

        // Remove items if it's the last step
        if (step >= maxStep)
            field.removeItems();

        view.showStatus(step, maxStep, field);
    }

    /**
     * Reset the simulation to a starting position.
     */
    public void reset ()
    {
        step = 0;
        characters.clear();
        populate();

        // Show the starting state in the view.
        view.showStatus(step, maxStep, field);
    }

    /**
     * Randomly populate the field with characters
     */
    private void populate ()
    {
        Random rand = Randomizer.getRandom();
        field.clear();
        for (int row = 0; row < field.getDepth(); row++)
        {
            for (int col = 0; col < field.getWidth(); col++)
            {
                if (rand.nextDouble() <= HUMAN_CREATION_PROBABILITY)
                {
                    Location location = new Location(row, col);
                    Human human = new Human(true, HUMAN_DEFAULT_HP, field, location);
                    characters.add(human);
                }
                else if (rand.nextDouble() <= VAMPIRE_CREATION_PROBABILITY)
                {
                    Location location = new Location(row, col);
                    Vampire vampire = new Vampire(VAMPIRE_DEFAULT_HP, field, location);
                    characters.add(vampire);
                }
                else if (rand.nextDouble() <= ZOMBIE_CREATION_PROBABILITY)
                {
                    Location location = new Location(row, col);
                    Zombie zombie = new Zombie(ZOMBIE_DEFAULT_HP, field, location);
                    characters.add(zombie);
                }
            }
        }
    }

    public static void main (String[] args)
    {
        Simulator sim = new Simulator(100, 100);
        sim.simulate(300);
    }
}
